The text is taken from OSTEP which is a free online textbook.
We are using the file to do some big (heavy) data analysis for CS744.
I hope there will be no copyright claims for this usage.
Thanks Remzi.

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES 
The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

The Abstraction: The Process
In this chapter, we discuss one of the most fundamental abstractions that
the OS provides to users: the process. The definition of a process, infor-
mally, is quite simple: it is a running program [V+65,BH70]. The program
itself is a lifeless thing: it just sits there on the disk, a bunch of instructions
(and maybe some static data), waiting to spring into action. It is the oper-
ating system that takes these bytes and gets them running, transforming
the program into something useful.
It turns out that one often wants to run more than one program at
once; for example, consider your desktop or laptop where you might like
to run a web browser, mail program, a game, a music player, and so forth.
In fact, a typical system may be seemingly running tens or even hundreds
of processes at the same time. Doing so makes the system easy to use, as
one never need be concerned with whether a CPU is available; one simply
runs programs. Hence our challenge:
THE CRUX OF THE PROBLEM:
HOW TO PROVIDE THE ILLUSION OF MANY CPUS?
Although there are only a few physical CPUs available, how can the
OS provide the illusion of a nearly-endless supply of said CPUs?
The OS creates this illusion by virtualizing the CPU. By running one
process, then stopping it and running another, and so forth, the OS can
promote the illusion that many virtual CPUs exist when in fact there is
only one physical CPU (or a few). This basic technique, known as time
sharing of the CPU, allows users to run as many concurrent processes as
they would like; the potential cost is performance, as each will run more
slowly if the CPU(s) must be shared.
To implement virtualization of the CPU, and to implement it well, the
OS will need both some low-level machinery and some high-level in-
telligence. We call the low-level machinery mechanisms; mechanisms
are low-level methods or protocols that implement a needed piece of
functionality. For example, we’ll learn later how to implement a context
1
2 THE ABSTRACTION: THE PROCESS
TIP: USE TIME SHARING (AND SPACE SHARING)
Time sharing is a basic technique used by an OS to share a resource. By
allowing the resource to be used for a little while by one entity, and then
a little while by another, and so forth, the resource in question (e.g., the
CPU, or a network link) can be shared by many. The counterpart of time
sharing is space sharing, where a resource is divided (in space) among
those who wish to use it. For example, disk space is naturally a space-
shared resource; once a block is assigned to a file, it is normally not as-
signed to another file until the user deletes the original file.
switch, which gives the OS the ability to stop running one program and
start running another on a given CPU; this time-sharing mechanism is
employed by all modern OSes.
On top of these mechanisms resides some of the intelligence in the
OS, in the form of policies. Policies are algorithms for making some
kind of decision within the OS. For example, given a number of possi-
ble programs to run on a CPU, which program should the OS run? A
scheduling policy in the OS will make this decision, likely using histori-
cal information (e.g., which program has run more over the last minute?),
workload knowledge (e.g., what types of programs are run), and perfor-
mance metrics (e.g., is the system optimizing for interactive performance,
or throughput?) to make its decision.
4.1 The Abstraction: A Process
The abstraction provided by the OS of a running program is something
we will call a process. As we said above, a process is simply a running
program; at any instant in time, we can summarize a process by taking an
inventory of the different pieces of the system it accesses or affects during
the course of its execution.
To understand what constitutes a process, we thus have to understand
its machine state: what a program can read or update when it is running.
At any given time, what parts of the machine are important to the execu-
tion of this program?
One obvious component of machine state that comprises a process is
its memory. Instructions lie in memory; the data that the running pro-
gram reads and writes sits in memory as well. Thus the memory that the
process can address (called its address space) is part of the process.
Also part of the process’s machine state are registers; many instructions
explicitly read or update registers and thus clearly they are important to
the execution of the process.
Note that there are some particularly special registers that form part
of this machine state. For example, the program counter (PC) (sometimes
called the instruction pointer or IP) tells us which instruction of the pro-
gram will execute next; similarly a stack pointer and associated frame
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 3
TIP: SEPARATE POLICY AND MECHANISM
In many operating systems, a common design paradigm is to separate
high-level policies from their low-level mechanisms [L+75]. You can
think of the mechanism as providing the answer to a how question about
a system; for example, how does an operating system perform a context
switch? The policy provides the answer to a which question; for example,
which process should the operating system run right now? Separating the
two allows one easily to change policies without having to rethink the
mechanism and is thus a form of modularity, a general software design
principle.
pointer are used to manage the stack for function parameters, local vari-
ables, and return addresses.
Finally, programs often access persistent storage devices too. Such I/O
information might include a list of the files the process currently has open.
4.2 Process API
Though we defer discussion of a real process API until a subsequent
chapter, here we first give some idea of what must be included in any
interface of an operating system. These APIs, in some form, are available
on any modern operating system.
• Create: An operating system must include some method to cre-
ate new processes. When you type a command into the shell, or
double-click on an application icon, the OS is invoked to create a
new process to run the program you have indicated.
• Destroy: As there is an interface for process creation, systems also
provide an interface to destroy processes forcefully. Of course, many
processes will run and just exit by themselves when complete; when
they don’t, however, the user may wish to kill them, and thus an in-
terface to halt a runaway process is quite useful.
• Wait: Sometimes it is useful to wait for a process to stop running;
thus some kind of waiting interface is often provided.
• Miscellaneous Control: Other than killing or waiting for a process,
there are sometimes other controls that are possible. For example,
most operating systems provide some kind of method to suspend a
process (stop it from running for a while) and then resume it (con-
tinue it running).
• Status: There are usually interfaces to get some status information
about a process as well, such as how long it has run for, or what
state it is in.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 THE ABSTRACTION: THE PROCESS
MemoryCPU
Disk
code
static data
heap
stack
Process
code
static data
Program Loading:
Takes on-disk program
and reads it into the
address space of process
Figure 4.1: Loading: From Program To Process
4.3 Process Creation: A Little More Detail
One mystery that we should unmask a bit is how programs are trans-
formed into processes. Specifically, how does the OS get a program up
and running? How does process creation actually work?
The first thing that the OS must do to run a program is to load its code
and any static data (e.g., initialized variables) into memory, into the ad-
dress space of the process. Programs initially reside on disk (or, in some
modern systems, flash-based SSDs) in some kind of executable format;
thus, the process of loading a program and static data into memory re-
quires the OS to read those bytes from disk and place them in memory
somewhere (as shown in Figure 4.1).
In early (or simple) operating systems, the loading process is done ea-
gerly, i.e., all at once before running the program; modern OSes perform
the process lazily, i.e., by loading pieces of code or data only as they are
needed during program execution. To truly understand how lazy loading
of pieces of code and data works, you’ll have to understand more about
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 5
the machinery of paging and swapping, topics we’ll cover in the future
when we discuss the virtualization of memory. For now, just remember
that before running anything, the OS clearly must do some work to get
the important program bits from disk into memory.
Once the code and static data are loaded into memory, there are a few
other things the OS needs to do before running the process. Some mem-
ory must be allocated for the program’s run-time stack (or just stack).
As you should likely already know, C programs use the stack for local
variables, function parameters, and return addresses; the OS allocates
this memory and gives it to the process. The OS will also likely initial-
ize the stack with arguments; specifically, it will fill in the parameters to
the main() function, i.e., argc and the argv array.
The OS may also allocate some memory for the program’s heap. In C
programs, the heap is used for explicitly requested dynamically-allocated
data; programs request such space by calling malloc() and free it ex-
plicitly by calling free(). The heap is needed for data structures such as
linked lists, hash tables, trees, and other interesting data structures. The
heap will be small at first; as the program runs, and requests more mem-
ory via the malloc() library API, the OS may get involved and allocate
more memory to the process to help satisfy such calls.
The OS will also do some other initialization tasks, particularly as re-
lated to input/output (I/O). For example, in UNIX systems, each process
by default has three open file descriptors, for standard input, output, and
error; these descriptors let programs easily read input from the terminal
and print output to the screen. We’ll learn more about I/O, file descrip-
tors, and the like in the third part of the book on persistence.
By loading the code and static data into memory, by creating and ini-
tializing a stack, and by doing other work as related to I/O setup, the OS
has now (finally) set the stage for program execution. It thus has one last
task: to start the program running at the entry point, namely main(). By
jumping to the main() routine (through a specialized mechanism that
we will discuss next chapter), the OS transfers control of the CPU to the
newly-created process, and thus the program begins its execution.
4.4 Process States
Now that we have some idea of what a process is (though we will
continue to refine this notion), and (roughly) how it is created, let us talk
about the different states a process can be in at a given time. The notion
that a process can be in one of these states arose in early computer systems
[DV66,V+65]. In a simplified view, a process can be in one of three states:
• Running: In the running state, a process is running on a processor.
This means it is executing instructions.
• Ready: In the ready state, a process is ready to run but for some
reason the OS has chosen not to run it at this given moment.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 THE ABSTRACTION: THE PROCESS
Running Ready
Blocked
Descheduled
Scheduled
I/O: initiate I/O: done
Figure 4.2: Process: State Transitions
• Blocked: In the blocked state, a process has performed some kind
of operation that makes it not ready to run until some other event
takes place. A common example: when a process initiates an I/O
request to a disk, it becomes blocked and thus some other process
can use the processor.
If we were to map these states to a graph, we would arrive at the di-
agram in Figure 4.2. As you can see in the diagram, a process can be
moved between the ready and running states at the discretion of the OS.
Being moved from ready to running means the process has been sched-
uled; being moved from running to ready means the process has been
descheduled. Once a process has become blocked (e.g., by initiating an
I/O operation), the OS will keep it as such until some event occurs (e.g.,
I/O completion); at that point, the process moves to the ready state again
(and potentially immediately to running again, if the OS so decides).
Let’s look at an example of how two processes might transition through
some of these states. First, imagine two processes running, each of which
only use the CPU (they do no I/O). In this case, a trace of the state of each
process might look like this (Figure 4.3).
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready
4 Running Ready Process0 now done
5 – Running
6 – Running
7 – Running
8 – Running Process1 now done
Figure 4.3: Tracing Process State: CPU Only
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 7
Time Process0 Process1 Notes
1 Running Ready
2 Running Ready
3 Running Ready Process0 initiates I/O
4 Blocked Running Process0 is blocked,
5 Blocked Running so Process1 runs
6 Blocked Running
7 Ready Running I/O done
8 Ready Running Process1 now done
9 Running –
10 Running – Process0 now done
Figure 4.4: Tracing Process State: CPU and I/O
In this next example, the first process issues an I/O after running for
some time. At that point, the process is blocked, giving the other process
a chance to run. Figure 4.4 shows a trace of this scenario.
More specifically, Process0 initiates an I/O and becomes blocked wait-
ing for it to complete; processes become blocked, for example, when read-
ing from a disk or waiting for a packet from a network. The OS recog-
nizes Process0 is not using the CPU and starts running Process1. While
Process1 is running, the I/O completes, moving Process0 back to ready.
Finally, Process1 finishes, and Process0 runs and then is done.
Note that there are many decisions the OS must make, even in this
simple example. First, the system had to decide to run Process1 while
Process0 issued an I/O; doing so improves resource utilization by keep-
ing the CPU busy. Second, the system decided not to switch back to
Process0 when its I/O completed; it is not clear if this is a good deci-
sion or not. What do you think? These types of decisions are made by the
OS scheduler, a topic we will discuss a few chapters in the future.
4.5 Data Structures
The OS is a program, and like any program, it has some key data struc-
tures that track various relevant pieces of information. To track the state
of each process, for example, the OS likely will keep some kind of pro-
cess list for all processes that are ready and some additional informa-
tion to track which process is currently running. The OS must also track,
in some way, blocked processes; when an I/O event completes, the OS
should make sure to wake the correct process and ready it to run again.
Figure 4.5 shows what type of information an OS needs to track about
each process in the xv6 kernel [CK+08]. Similar process structures exist
in “real” operating systems such as Linux, Mac OS X, or Windows; look
them up and see how much more complex they are.
From the figure, you can see a couple of important pieces of informa-
tion the OS tracks about a process. The register context will hold, for a
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 THE ABSTRACTION: THE PROCESS
// the registers xv6 will save and restore
// to stop and subsequently restart a process
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the different states a process can be in
enum proc_state { UNUSED, EMBRYO, SLEEPING,
RUNNABLE, RUNNING, ZOMBIE };
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
uint sz; // Size of process memory
char *kstack; // Bottom of kernel stack
// for this process
enum proc_state state; // Process state
int pid; // Process ID
struct proc *parent; // Parent process
void *chan; // If !zero, sleeping on chan
int killed; // If !zero, has been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for the
// current interrupt
};
Figure 4.5: The xv6 Proc Structure
stopped process, the contents of its registers. When a process is stopped,
its registers will be saved to this memory location; by restoring these reg-
isters (i.e., placing their values back into the actual physical registers), the
OS can resume running the process. We’ll learn more about this technique
known as a context switch in future chapters.
You can also see from the figure that there are some other states a pro-
cess can be in, beyond running, ready, and blocked. Sometimes a system
will have an initial state that the process is in when it is being created.
Also, a process could be placed in a final state where it has exited but
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 9
ASIDE: DATA STRUCTURE — THE PROCESS LIST
Operating systems are replete with various important data structures
that we will discuss in these notes. The process list (also called the task
list) is the first such structure. It is one of the simpler ones, but certainly
any OS that has the ability to run multiple programs at once will have
something akin to this structure in order to keep track of all the running
programs in the system. Sometimes people refer to the individual struc-
ture that stores information about a process as a Process Control Block
(PCB), a fancy way of talking about a C structure that contains informa-
tion about each process (also sometimes called a process descriptor).
has not yet been cleaned up (in UNIX-based systems, this is called the
zombie state1). This final state can be useful as it allows other processes
(usually the parent that created the process) to examine the return code
of the process and see if the just-finished process executed successfully
(usually, programs return zero in UNIX-based systems when they have
accomplished a task successfully, and non-zero otherwise). When fin-
ished, the parent will make one final call (e.g., wait()) to wait for the
completion of the child, and to also indicate to the OS that it can clean up
any relevant data structures that referred to the now-extinct process.
4.6 Summary
We have introduced the most basic abstraction of the OS: the process.
It is quite simply viewed as a running program. With this conceptual
view in mind, we will now move on to the nitty-gritty: the low-level
mechanisms needed to implement processes, and the higher-level poli-
cies required to schedule them in an intelligent way. By combining mech-
anisms and policies, we will build up our understanding of how an oper-
ating system virtualizes the CPU.
1Yes, the zombie state. Just like real zombies, these zombies are relatively easy to kill.
However, different techniques are usually recommended.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 THE ABSTRACTION: THE PROCESS
ASIDE: KEY PROCESS TERMS
• The process is the major OS abstraction of a running program. At
any point in time, the process can be described by its state: the con-
tents of memory in its address space, the contents of CPU registers
(including the program counter and stack pointer, among others),
and information about I/O (such as open files which can be read or
written).
• The process API consists of calls programs can make related to pro-
cesses. Typically, this includes creation, destruction, and other use-
ful calls.
• Processes exist in one of many different process states, including
running, ready to run, and blocked. Different events (e.g., getting
scheduled or descheduled, or waiting for an I/O to complete) tran-
sition a process from one of these states to the other.
• A process list contains information about all processes in the sys-
tem. Each entry is found in what is sometimes called a process
control block (PCB), which is really just a structure that contains
information about a specific process.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 11
References
[BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communica-
tions of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in
operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that
rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein.
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai
Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the
world. Download and play with it to learn more about the details of how operating systems actually
work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the
book may not match the latest in the source.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis,
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper
defined many of the early terms and concepts around building multiprogrammed systems.
[L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack,
W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operat-
ing systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of
its ideas influenced OS designers.
[V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham.
Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic
ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are
finally being realized in modern cloud systems.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 THE ABSTRACTION: THE PROCESS
Homework (Simulation)
This program, process-run.py, allows you to see how process states
change as programs run and either use the CPU (e.g., perform an add
instruction) or do I/O (e.g., send a request to a disk and wait for it to
complete). See the README for details.
Questions
1. Run process-run.py with the following flags: -l 5:100,5:100.
What should the CPU utilization be (e.g., the percent of time the
CPU is in use?) Why do you know this? Use the -c and -p flags to
see if you were right.
2. Now run with these flags: ./process-run.py -l 4:100,1:0.
These flags specify one process with 4 instructions (all to use the
CPU), and one that simply issues an I/O and waits for it to be done.
How long does it take to complete both processes? Use -c and -p
to find out if you were right.
3. Switch the order of the processes: -l 1:0,4:100. What happens
now? Does switching the order matter? Why? (As always, use -c
and -p to see if you were right)
4. We’ll now explore some of the other flags. One important flag is
-S, which determines how the system reacts when a process is-
sues an I/O. With the flag set to SWITCH ON END, the system
will NOT switch to another process while one is doing I/O, in-
stead waiting until the process is completely finished. What hap-
pens when you run the following two processes (-l 1:0,4:100
-c -S SWITCH ON END), one doing I/O and the other doing CPU
work?
5. Now, run the same processes, but with the switching behavior set
to switch to another process whenever one is WAITING for I/O (-l
1:0,4:100 -c -S SWITCH ON IO). What happens now? Use -c
and -p to confirm that you are right.
6. One other important behavior is what to do when an I/O com-
pletes. With -I IO RUN LATER, when an I/O completes, the pro-
cess that issued it is not necessarily run right away; rather, what-
ever was running at the time keeps running. What happens when
you run this combination of processes? (./process-run.py -l
3:0,5:100,5:100,5:100 -S SWITCH ON IO -c -p -I
IO RUN LATER) Are system resources being effectively utilized?
7. Now run the same processes, but with -I IO RUN IMMEDIATE set,
which immediately runs the process that issued the I/O. How does
this behavior differ? Why might running a process that just com-
pleted an I/O again be a good idea?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
THE ABSTRACTION: THE PROCESS 13
8. Now run with some randomly generated processes using flags -s
1 -l 3:50,3:50 or -s 2 -l 3:50,3:50 or -s 3 -l 3:50,
3:50. See if you can predict how the trace will turn out. What hap-
pens when you use the flag -I IO RUN IMMEDIATE versus that
flag -I IO RUN LATER? What happens when you use the flag -S
SWITCH ON IO versus -S SWITCH ON END?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES

5
Interlude: Process API
ASIDE: INTERLUDES
Interludes will cover more practical aspects of systems, including a par-
ticular focus on operating system APIs and how to use them. If you don’t
like practical things, you could skip these interludes. But you should like
practical things, because, well, they are generally useful in real life; com-
panies, for example, don’t usually hire you for your non-practical skills.
In this interlude, we discuss process creation in UNIX systems. UNIX
presents one of the most intriguing ways to create a new process with
a pair of system calls: fork() and exec(). A third routine, wait(),
can be used by a process wishing to wait for a process it has created to
complete. We now present these interfaces in more detail, with a few
simple examples to motivate us. And thus, our problem:
CRUX: HOW TO CREATE AND CONTROL PROCESSES
What interfaces should the OS present for process creation and con-
trol? How should these interfaces be designed to enable powerful func-
tionality, ease of use, and high performance?
5.1 The fork() System Call
The fork() system call is used to create a new process [C63]. How-
ever, be forewarned: it is certainly the strangest routine you will ever
call1. More specifically, you have a running program whose code looks
like what you see in Figure 5.1; examine the code, or better yet, type it in
and run it yourself!
1Well, OK, we admit that we don’t know that for sure; who knows what routines you
call when no one is looking? But fork() is pretty odd, no matter how unusual your routine-
calling patterns are.
1
2 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 int main(int argc, char *argv[]) {
6 printf("hello (pid:%d)\n", (int) getpid());
7 int rc = fork();
8 if (rc < 0) {
9 // fork failed
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) {
13 // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 } else {
16 // parent goes down this path (main)
17 printf("parent of %d (pid:%d)\n",
18 rc, (int) getpid());
19 }
20 return 0;
21 }
22
Figure 5.1: Calling fork() (p1.c)
When you run this program (called p1.c), you’ll see the following:
prompt> ./p1
hello (pid:29146)
parent of 29147 (pid:29146)
child (pid:29147)
prompt>
Let us understand what happened in more detail in p1.c. When it
first started running, the process prints out a hello message; included in
that message is its process identifier, also known as a PID. The process
has a PID of 29146; in UNIX systems, the PID is used to name the process
if one wants to do something with the process, such as (for example) stop
it from running. So far, so good.
Now the interesting part begins. The process calls the fork() system
call, which the OS provides as a way to create a new process. The odd
part: the process that is created is an (almost) exact copy of the calling pro-
cess. That means that to the OS, it now looks like there are two copies of
the program p1 running, and both are about to return from the fork()
system call. The newly-created process (called the child, in contrast to the
creating parent) doesn’t start running at main(), like you might expect
(note, the “hello” message only got printed out once); rather, it just comes
into life as if it had called fork() itself.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 3
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(int argc, char *argv[]) {
7 printf("hello (pid:%d)\n", (int) getpid());
8 int rc = fork();
9 if (rc < 0) { // fork failed; exit
10 fprintf(stderr, "fork failed\n");
11 exit(1);
12 } else if (rc == 0) { // child (new process)
13 printf("child (pid:%d)\n", (int) getpid());
14 } else { // parent goes down this path
15 int rc_wait = wait(NULL);
16 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
17 rc, rc_wait, (int) getpid());
18 }
19 return 0;
20 }
21
Figure 5.2: Calling fork() And wait() (p2.c)
You might have noticed: the child isn’t an exact copy. Specifically, al-
though it now has its own copy of the address space (i.e., its own private
memory), its own registers, its own PC, and so forth, the value it returns
to the caller of fork() is different. Specifically, while the parent receives
the PID of the newly-created child, the child receives a return code of
zero. This differentiation is useful, because it is simple then to write the
code that handles the two different cases (as above).
You might also have noticed: the output (of p1.c) is not deterministic.
When the child process is created, there are now two active processes in
the system that we care about: the parent and the child. Assuming we
are running on a system with a single CPU (for simplicity), then either
the child or the parent might run at that point. In our example (above),
the parent did and thus printed out its message first. In other cases, the
opposite might happen, as we show in this output trace:
prompt> ./p1
hello (pid:29146)
child (pid:29147)
parent of 29147 (pid:29146)
prompt>
The CPU scheduler, a topic we’ll discuss in great detail soon, deter-
mines which process runs at a given moment in time; because the sched-
uler is complex, we cannot usually make strong assumptions about what
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 INTERLUDE: PROCESS API
it will choose to do, and hence which process will run first. This non-
determinism, as it turns out, leads to some interesting problems, par-
ticularly in multi-threaded programs; hence, we’ll see a lot more non-
determinism when we study concurrency in the second part of the book.
5.2 The wait() System Call
So far, we haven’t done much: just created a child that prints out a
message and exits. Sometimes, as it turns out, it is quite useful for a
parent to wait for a child process to finish what it has been doing. This
task is accomplished with the wait() system call (or its more complete
sibling waitpid()); see Figure 5.2 for details.
In this example (p2.c), the parent process calls wait() to delay its
execution until the child finishes executing. When the child is done,
wait() returns to the parent.
Adding a wait() call to the code above makes the output determin-
istic. Can you see why? Go ahead, think about it.
(waiting for you to think .... and done)
Now that you have thought a bit, here is the output:
prompt> ./p2
hello (pid:29266)
child (pid:29267)
parent of 29267 (rc_wait:29267) (pid:29266)
prompt>
With this code, we now know that the child will always print first.
Why do we know that? Well, it might simply run first, as before, and
thus print before the parent. However, if the parent does happen to run
first, it will immediately call wait(); this system call won’t return until
the child has run and exited2. Thus, even when the parent runs first, it
politely waits for the child to finish running, then wait() returns, and
then the parent prints its message.
5.3 Finally, The exec() System Call
A final and important piece of the process creation API is the exec()
system call3. This system call is useful when you want to run a program
that is different from the calling program. For example, calling fork()
2There are a few cases where wait() returns before the child exits; read the man page
for more details, as always. And beware of any absolute and unqualified statements this book
makes, such as “the child will always print first” or “UNIX is the best thing in the world, even
better than ice cream.”
3On Linux, there are six variants of exec(): execl(), execlp(), execle(),
execv(), execvp(), and execvpe(). Read the man pages to learn more.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 5
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6
7 int main(int argc, char *argv[]) {
8 printf("hello (pid:%d)\n", (int) getpid());
9 int rc = fork();
10 if (rc < 0) { // fork failed; exit
11 fprintf(stderr, "fork failed\n");
12 exit(1);
13 } else if (rc == 0) { // child (new process)
14 printf("child (pid:%d)\n", (int) getpid());
15 char *myargs[3];
16 myargs[0] = strdup("wc"); // program: "wc"
17 myargs[1] = strdup("p3.c"); // arg: input file
18 myargs[2] = NULL; // mark end of array
19 execvp(myargs[0], myargs); // runs word count
20 printf("this shouldn’t print out");
21 } else { // parent goes down this path
22 int rc_wait = wait(NULL);
23 printf("parent of %d (rc_wait:%d) (pid:%d)\n",
24 rc, rc_wait, (int) getpid());
25 }
26 return 0;
27 }
28
Figure 5.3: Calling fork(), wait(), And exec() (p3.c)
in p2.c is only useful if you want to keep running copies of the same
program. However, often you want to run a different program; exec()
does just that (Figure 5.3).
In this example, the child process calls execvp() in order to run the
program wc, which is the word counting program. In fact, it runs wc on
the source file p3.c, thus telling us how many lines, words, and bytes are
found in the file:
prompt> ./p3
hello (pid:29383)
child (pid:29384)
29 107 1030 p3.c
parent of 29384 (rc_wait:29384) (pid:29383)
prompt>
The fork() system call is strange; its partner in crime, exec(), is not
so normal either. What it does: given the name of an executable (e.g., wc),
and some arguments (e.g., p3.c), it loads code (and static data) from that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 INTERLUDE: PROCESS API
TIP: GETTING IT RIGHT (LAMPSON’S LAW)
As Lampson states in his well-regarded “Hints for Computer Systems
Design” [L83], “Get it right. Neither abstraction nor simplicity is a sub-
stitute for getting it right.” Sometimes, you just have to do the right thing,
and when you do, it is way better than the alternatives. There are lots
of ways to design APIs for process creation; however, the combination
of fork() and exec() are simple and immensely powerful. Here, the
UNIX designers simply got it right. And because Lampson so often “got
it right”, we name the law in his honor.
executable and overwrites its current code segment (and current static
data) with it; the heap and stack and other parts of the memory space of
the program are re-initialized. Then the OS simply runs that program,
passing in any arguments as the argv of that process. Thus, it does not
create a new process; rather, it transforms the currently running program
(formerly p3) into a different running program (wc). After the exec()
in the child, it is almost as if p3.c never ran; a successful call to exec()
never returns.
5.4 Why? Motivating The API
Of course, one big question you might have: why would we build
such an odd interface to what should be the simple act of creating a new
process? Well, as it turns out, the separation of fork() and exec() is
essential in building a UNIX shell, because it lets the shell run code after
the call to fork() but before the call to exec(); this code can alter the
environment of the about-to-be-run program, and thus enables a variety
of interesting features to be readily built.
The shell is just a user program4. It shows you a prompt and then
waits for you to type something into it. You then type a command (i.e.,
the name of an executable program, plus any arguments) into it; in most
cases, the shell then figures out where in the file system the executable
resides, calls fork() to create a new child process to run the command,
calls some variant of exec() to run the command, and then waits for the
command to complete by calling wait(). When the child completes, the
shell returns from wait() and prints out a prompt again, ready for your
next command.
The separation of fork() and exec() allows the shell to do a whole
bunch of useful things rather easily. For example:
prompt> wc p3.c > newfile.txt
4And there are lots of shells; tcsh, bash, and zsh to name a few. You should pick one,
read its man pages, and learn more about it; all UNIX experts do.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 7
In the example above, the output of the program wc is redirected into
the output file newfile.txt (the greater-than sign is how said redirec-
tion is indicated). The way the shell accomplishes this task is quite sim-
ple: when the child is created, before calling exec(), the shell (specifi-
cally, the code executed in the child process) closes standard output and
opens the file newfile.txt. By doing so, any output from the soon-
to-be-running program wc is sent to the file instead of the screen (open
file descriptors are kept open across the exec() call, thus enabling this
behavior [SR05]).
Figure 5.4 (page 8) shows a program that does exactly this. The reason
this redirection works is due to an assumption about how the operating
system manages file descriptors. Specifically, UNIX systems start looking
for free file descriptors at zero. In this case, STDOUT FILENO will be the
first available one and thus get assigned when open() is called. Subse-
quent writes by the child process to the standard output file descriptor,
for example by routines such as printf(), will then be routed transpar-
ently to the newly-opened file instead of the screen.
Here is the output of running the p4.c program:
prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c
prompt>
You’ll notice (at least) two interesting tidbits about this output. First,
when p4 is run, it looks as if nothing has happened; the shell just prints
the command prompt and is immediately ready for your next command.
However, that is not the case; the program p4 did indeed call fork() to
create a new child, and then run the wc program via a call to execvp().
You don’t see any output printed to the screen because it has been redi-
rected to the file p4.output. Second, you can see that when we cat the
output file, all the expected output from running wc is found. Cool, right?
UNIX pipes are implemented in a similar way, but with the pipe()
system call. In this case, the output of one process is connected to an in-
kernel pipe (i.e., queue), and the input of another process is connected
to that same pipe; thus, the output of one process seamlessly is used as
input to the next, and long and useful chains of commands can be strung
together. As a simple example, consider looking for a word in a file, and
then counting how many times said word occurs; with pipes and the util-
ities grep and wc, it is easy; just type grep -o foo file | wc -l
into the command prompt and marvel at the result.
Finally, while we just have sketched out the process API at a high level,
there is a lot more detail about these calls out there to be learned and
digested; we’ll learn more, for example, about file descriptors when we
talk about file systems in the third part of the book. For now, suffice it
to say that the fork()/exec() combination is a powerful way to create
and manipulate processes.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 INTERLUDE: PROCESS API
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #include <sys/wait.h>
7
8 int main(int argc, char *argv[]) {
9 int rc = fork();
10 if (rc < 0) {
11 // fork failed
12 fprintf(stderr, "fork failed\n");
13 exit(1);
14 } else if (rc == 0) {
15 // child: redirect standard output to a file
16 close(STDOUT_FILENO);
17 open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC,
18 S_IRWXU);
19 // now exec "wc"...
20 char *myargs[3];
21 myargs[0] = strdup("wc"); // program: wc
22 myargs[1] = strdup("p4.c"); // arg: file to count
23 myargs[2] = NULL; // mark end of array
24 execvp(myargs[0], myargs); // runs word count
25 } else {
26 // parent goes down this path (main)
27 int rc_wait = wait(NULL);
28 }
29 return 0;
30 }
Figure 5.4: All Of The Above With Redirection (p4.c)
5.5 Process Control And Users
Beyond fork(), exec(), and wait(), there are a lot of other inter-
faces for interacting with processes in UNIX systems. For example, the
kill() system call is used to send signals to a process, including di-
rectives to pause, die, and other useful imperatives. For convenience,
in most UNIX shells, certain keystroke combinations are configured to
deliver a specific signal to the currently running process; for example,
control-c sends a SIGINT (interrupt) to the process (normally terminating
it) and control-z sends a SIGTSTP (stop) signal thus pausing the process
in mid-execution (you can resume it later with a command, e.g., the fg
built-in command found in many shells).
The entire signals subsystem provides a rich infrastructure to deliver
external events to processes, including ways to receive and process those
signals within individual processes, and ways to send signals to individ-
ual processes as well as entire process groups. To use this form of com-
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 9
ASIDE: RTFM — READ THE MAN PAGES
Many times in this book, when referring to a particular system call or
library call, we’ll tell you to read the manual pages, or man pages for
short. Man pages are the original form of documentation that exist on
UNIX systems; realize that they were created before the thing called the
web existed.
Spending some time reading man pages is a key step in the growth of
a systems programmer; there are tons of useful tidbits hidden in those
pages. Some particularly useful pages to read are the man pages for
whichever shell you are using (e.g., tcsh, or bash), and certainly for any
system calls your program makes (in order to see what return values and
error conditions exist).
Finally, reading the man pages can save you some embarrassment. When
you ask colleagues about some intricacy of fork(), they may simply
reply: “RTFM.” This is your colleagues’ way of gently urging you to Read
The Man pages. The F in RTFM just adds a little color to the phrase...
munication, a process should use the signal() system call to “catch”
various signals; doing so ensures that when a particular signal is deliv-
ered to a process, it will suspend its normal execution and run a particu-
lar piece of code in response to the signal. Read elsewhere [SR05] to learn
more about signals and their many intricacies.
This naturally raises the question: who can send a signal to a process,
and who cannot? Generally, the systems we use can have multiple people
using them at the same time; if one of these people can arbitrarily send
signals such as SIGINT (to interrupt a process, likely terminating it), the
usability and security of the system will be compromised. As a result,
modern systems include a strong conception of the notion of a user. The
user, after entering a password to establish credentials, logs in to gain
access to system resources. The user may then launch one or many pro-
cesses, and exercise full control over them (pause them, kill them, etc.).
Users generally can only control their own processes; it is the job of the
operating system to parcel out resources (such as CPU, memory, and disk)
to each user (and their processes) to meet overall system goals.
5.6 Useful Tools
There are many command-line tools that are useful as well. For exam-
ple, using the ps command allows you to see which processes are run-
ning; read the man pages for some useful flags to pass to ps. The tool top
is also quite helpful, as it displays the processes of the system and how
much CPU and other resources they are eating up. Humorously, many
times when you run it, top claims it is the top resource hog; perhaps it is
a bit of an egomaniac. The command kill can be used to send arbitrary
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 INTERLUDE: PROCESS API
ASIDE: THE SUPERUSER (ROOT)
A system generally needs a user who can administer the system, and is
not limited in the way most users are. Such a user should be able to kill
an arbitrary process (e.g., if it is abusing the system in some way), even
though that process was not started by this user. Such a user should also
be able to run powerful commands such as shutdown (which, unsurpris-
ingly, shuts down the system). In UNIX-based systems, these special abil-
ities are given to the superuser (sometimes called root). While most users
can’t kill other users processes, the superuser can. Being root is much like
being Spider-Man: with great power comes great responsibility [QI15].
Thus, to increase security (and avoid costly mistakes), it’s usually better
to be a regular user; if you do need to be root, tread carefully, as all of the
destructive powers of the computing world are now at your fingertips.
signals to processes, as can the slightly more user friendly killall. Be
sure to use these carefully; if you accidentally kill your window manager,
the computer you are sitting in front of may become quite difficult to use.
Finally, there are many different kinds of CPU meters you can use to
get a quick glance understanding of the load on your system; for example,
we always keep MenuMeters (from Raging Menace software) running on
our Macintosh toolbars, so we can see how much CPU is being utilized
at any moment in time. In general, the more information about what is
going on, the better.
5.7 Summary
We have introduced some of the APIs dealing with UNIX process cre-
ation: fork(), exec(), and wait(). However, we have just skimmed
the surface. For more detail, read Stevens and Rago [SR05], of course,
particularly the chapters on Process Control, Process Relationships, and
Signals; there is much to extract from the wisdom therein.
While our passion for the UNIX process API remains strong, we should
also note that such positivity is not uniform. For example, a recent pa-
per by systems researchers from Microsoft, Boston University, and ETH
in Switzerland details some problems with fork(), and advocates for
other, simpler process creation APIs such as spawn() [B+19]. Read it,
and the related work it refers to, to understand this different vantage
point. While it’s generally good to trust this book, remember too that
the authors have opinions; those opinions may not (always) be as widely
shared as you might think.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 11
ASIDE: KEY PROCESS API TERMS
• Each process has a name; in most systems, that name is a number
known as a process ID (PID).
• The fork() system call is used in UNIX systems to create a new pro-
cess. The creator is called the parent; the newly created process is
called the child. As sometimes occurs in real life [J16], the child
process is a nearly identical copy of the parent.
• The wait() system call allows a parent to wait for its child to com-
plete execution.
• The exec() family of system calls allows a child to break free from
its similarity to its parent and execute an entirely new program.
• A UNIX shell commonly uses fork(), wait(), and exec() to
launch user commands; the separation of fork and exec enables fea-
tures like input/output redirection, pipes, and other cool features,
all without changing anything about the programs being run.
• Process control is available in the form of signals, which can cause
jobs to stop, continue, or even terminate.
• Which processes can be controlled by a particular person is encap-
sulated in the notion of a user; the operating system allows multiple
users onto the system, and ensures users can only control their own
processes.
• A superuser can control all processes (and indeed do many other
things); this role should be assumed infrequently and with caution
for security reasons.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 INTERLUDE: PROCESS API
References
[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Tim-
othy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an
opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where
systems researchers go to present extreme opinions in the hopes of pushing the community in new di-
rections.
[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer
Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may
be the first place the term fork() was used in the discussion of spawning new processes.
[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and
Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic
paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence
on Project MAC, Multics, and eventually UNIX.
[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This
hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind
of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come
back here! This, in a microcosm, is the danger of surfing the web.
[L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems
Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems.
You should read it at some point in your life, and probably at many points in your life.
[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available:
https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator
concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French
National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit´e est la
suite ins´eparable d’un grand pouvoir”, which roughly translates to “They must consider that great
responsibility follows inseparably from great power.” Only in 1962 did the following words appear in
Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French
Revolution gets credit for this one, not Stan Lee. Sorry, Stan.
[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen
A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein.
Buy this book! Read it! And most importantly, live it.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 13
Homework (Simulation)
This simulation homework focuses on fork.py, a simple process cre-
ation simulator that shows how processes are related in a single “famil-
ial” tree. Read the relevant README for details about how to run the
simulator.
Questions
1. Run ./fork.py -s 10 and see which actions are taken. Can you
predict what the process tree looks like at each step? Use the -c
flag to check your answers. Try some different random seeds (-s)
or add more actions (-a) to get the hang of it.
2. One control the simulator gives you is the fork percentage, con-
trolled by the -f flag. The higher it is, the more likely the next
action is a fork; the lower it is, the more likely the action is an
exit. Run the simulator with a large number of actions (e.g., -a
100) and vary the fork percentage from 0.1 to 0.9. What do you
think the resulting final process trees will look like as the percent-
age changes? Check your answer with -c.
3. Now, switch the output by using the -t flag (e.g., run ./fork.py
-t). Given a set of process trees, can you tell which actions were
taken?
4. One interesting thing to note is what happens when a child exits;
what happens to its children in the process tree? To study this, let’s
create a specific example: ./fork.py -A a+b,b+c,c+d,c+e,c-.
This example has process ’a’ create ’b’, which in turn creates ’c’,
which then creates ’d’ and ’e’. However, then, ’c’ exits. What do
you think the process tree should like after the exit? What if you
use the -R flag? Learn more about what happens to orphaned pro-
cesses on your own to add more context.
5. One last flag to explore is the -F flag, which skips intermediate
steps and only asks to fill in the final process tree. Run ./fork.py
-F and see if you can write down the final tree by looking at the
series of actions generated. Use different random seeds to try this a
few times.
6. Finally, use both -t and -F together. This shows the final process
tree, but then asks you to fill in the actions that took place. By look-
ing at the tree, can you determine the exact actions that took place?
In which cases can you tell? In which can’t you tell? Try some dif-
ferent random seeds to delve into this question.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 INTERLUDE: PROCESS API
ASIDE: CODING HOMEWORKS
Coding homeworks are small exercises where you write code to run on
a real machine to get some experience with some basic operating system
APIs. After all, you are (probably) a computer scientist, and therefore
should like to code, right? If you don’t, there is always CS theory, but
that’s pretty hard. Of course, to truly become an expert, you have to
spend more than a little time hacking away at the machine; indeed, find
every excuse you can to write some code and see how it works. Spend
the time, and become the wise master you know you can be.
Homework (Code)
In this homework, you are to gain some familiarity with the process
management APIs about which you just read. Don’t worry – it’s even
more fun than it sounds! You’ll in general be much better off if you find
as much time as you can to write some code, so why not start now?
Questions
1. Write a program that calls fork(). Before calling fork(), have the
main process access a variable (e.g., x) and set its value to some-
thing (e.g., 100). What value is the variable in the child process?
What happens to the variable when both the child and parent change
the value of x?
2. Write a program that opens a file (with the open() system call)
and then calls fork() to create a new process. Can both the child
and parent access the file descriptor returned by open()? What
happens when they are writing to the file concurrently, i.e., at the
same time?
3. Write another program using fork(). The child process should
print “hello”; the parent process should print “goodbye”. You should
try to ensure that the child process always prints first; can you do
this without calling wait() in the parent?
4. Write a program that calls fork() and then calls some form of
exec() to run the program /bin/ls. See if you can try all of the
variants of exec(), including (on Linux) execl(), execle(),
execlp(), execv(), execvp(), and execvpe(). Why do
you think there are so many variants of the same basic call?
5. Now write a program that uses wait() to wait for the child process
to finish in the parent. What does wait() return? What happens if
you use wait() in the child?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
INTERLUDE: PROCESS API 15
6. Write a slight modification of the previous program, this time us-
ing waitpid() instead of wait(). When would waitpid() be
useful?
7. Write a program that creates a child process, and then in the child
closes standard output (STDOUT FILENO). What happens if the child
calls printf() to print some output after closing the descriptor?
8. Write a program that creates two children, and connects the stan-
dard output of one to the standard input of the other, using the
pipe() system call.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES


6
Mechanism: Limited Direct Execution
In order to virtualize the CPU, the operating system needs to somehow
share the physical CPU among many jobs running seemingly at the same
time. The basic idea is simple: run one process for a little while, then
run another one, and so forth. By time sharing the CPU in this manner,
virtualization is achieved.
There are a few challenges, however, in building such virtualization
machinery. The first is performance: how can we implement virtualiza-
tion without adding excessive overhead to the system? The second is
control: how can we run processes efficiently while retaining control over
the CPU? Control is particularly important to the OS, as it is in charge of
resources; without control, a process could simply run forever and take
over the machine, or access information that it should not be allowed to
access. Obtaining high performance while maintaining control is thus
one of the central challenges in building an operating system.
THE CRUX:
HOW TO EFFICIENTLY VIRTUALIZE THE CPU WITH CONTROL
The OS must virtualize the CPU in an efficient manner while retaining
control over the system. To do so, both hardware and operating-system
support will be required. The OS will often use a judicious bit of hard-
ware support in order to accomplish its work effectively.
6.1 Basic Technique: Limited Direct Execution
To make a program run as fast as one might expect, not surprisingly
OS developers came up with a technique, which we call limited direct
execution. The “direct execution” part of the idea is simple: just run the
program directly on the CPU. Thus, when the OS wishes to start a pro-
gram running, it creates a process entry for it in a process list, allocates
some memory for it, loads the program code into memory (from disk), lo-
cates its entry point (i.e., the main() routine or something similar), jumps
1
2 MECHANISM: LIMITED DIRECT EXECUTION
OS Program
Create entry for process list
Allocate memory for program
Load program into memory
Set up stack with argc/argv
Clear registers
Execute call main()
Run main()
Execute return from main
Free memory of process
Remove from process list
Figure 6.1: Direct Execution Protocol (Without Limits)
to it, and starts running the user’s code. Figure 6.1 shows this basic di-
rect execution protocol (without any limits, yet), using a normal call and
return to jump to the program’s main() and later back into the kernel.
Sounds simple, no? But this approach gives rise to a few problems
in our quest to virtualize the CPU. The first is simple: if we just run a
program, how can the OS make sure the program doesn’t do anything
that we don’t want it to do, while still running it efficiently? The second:
when we are running a process, how does the operating system stop it
from running and switch to another process, thus implementing the time
sharing we require to virtualize the CPU?
In answering these questions below, we’ll get a much better sense of
what is needed to virtualize the CPU. In developing these techniques,
we’ll also see where the “limited” part of the name arises from; without
limits on running programs, the OS wouldn’t be in control of anything
and thus would be “just a library” — a very sad state of affairs for an
aspiring operating system!
6.2 Problem #1: Restricted Operations
Direct execution has the obvious advantage of being fast; the program
runs natively on the hardware CPU and thus executes as quickly as one
would expect. But running on the CPU introduces a problem: what if
the process wishes to perform some kind of restricted operation, such
as issuing an I/O request to a disk, or gaining access to more system
resources such as CPU or memory?
THE CRUX: HOW TO PERFORM RESTRICTED OPERATIONS
A process must be able to perform I/O and some other restricted oper-
ations, but without giving the process complete control over the system.
How can the OS and hardware work together to do so?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 3
ASIDE: WHY SYSTEM CALLS LOOK LIKE PROCEDURE CALLS
You may wonder why a call to a system call, such as open() or read(),
looks exactly like a typical procedure call in C; that is, if it looks just like
a procedure call, how does the system know it’s a system call, and do all
the right stuff? The simple reason: it is a procedure call, but hidden in-
side that procedure call is the famous trap instruction. More specifically,
when you call open() (for example), you are executing a procedure call
into the C library. Therein, whether for open() or any of the other sys-
tem calls provided, the library uses an agreed-upon calling convention
with the kernel to put the arguments to open() in well-known locations
(e.g., on the stack, or in specific registers), puts the system-call number
into a well-known location as well (again, onto the stack or a register),
and then executes the aforementioned trap instruction. The code in the
library after the trap unpacks return values and returns control to the
program that issued the system call. Thus, the parts of the C library that
make system calls are hand-coded in assembly, as they need to carefully
follow convention in order to process arguments and return values cor-
rectly, as well as execute the hardware-specific trap instruction. And now
you know why you personally don’t have to write assembly code to trap
into an OS; somebody has already written that assembly for you.
One approach would simply be to let any process do whatever it wants
in terms of I/O and other related operations. However, doing so would
prevent the construction of many kinds of systems that are desirable. For
example, if we wish to build a file system that checks permissions before
granting access to a file, we can’t simply let any user process issue I/Os
to the disk; if we did, a process could simply read or write the entire disk
and thus all protections would be lost.
Thus, the approach we take is to introduce a new processor mode,
known as user mode; code that runs in user mode is restricted in what it
can do. For example, when running in user mode, a process can’t issue
I/O requests; doing so would result in the processor raising an exception;
the OS would then likely kill the process.
In contrast to user mode is kernel mode, which the operating system
(or kernel) runs in. In this mode, code that runs can do what it likes, in-
cluding privileged operations such as issuing I/O requests and executing
all types of restricted instructions.
We are still left with a challenge, however: what should a user pro-
cess do when it wishes to perform some kind of privileged operation,
such as reading from disk? To enable this, virtually all modern hard-
ware provides the ability for user programs to perform a system call.
Pioneered on ancient machines such as the Atlas [K+61,L78], system calls
allow the kernel to carefully expose certain key pieces of functionality to
user programs, such as accessing the file system, creating and destroy-
ing processes, communicating with other processes, and allocating more
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 MECHANISM: LIMITED DIRECT EXECUTION
TIP: USE PROTECTED CONTROL TRANSFER
The hardware assists the OS by providing different modes of execution.
In user mode, applications do not have full access to hardware resources.
In kernel mode, the OS has access to the full resources of the machine.
Special instructions to trap into the kernel and return-from-trap back to
user-mode programs are also provided, as well as instructions that allow
the OS to tell the hardware where the trap table resides in memory.
memory. Most operating systems provide a few hundred calls (see the
POSIX standard for details [P10]); early Unix systems exposed a more
concise subset of around twenty calls.
To execute a system call, a program must execute a special trap instruc-
tion. This instruction simultaneously jumps into the kernel and raises the
privilege level to kernel mode; once in the kernel, the system can now per-
form whatever privileged operations are needed (if allowed), and thus do
the required work for the calling process. When finished, the OS calls a
special return-from-trap instruction, which, as you might expect, returns
into the calling user program while simultaneously reducing the privi-
lege level back to user mode.
The hardware needs to be a bit careful when executing a trap, in that it
must make sure to save enough of the caller’s registers in order to be able
to return correctly when the OS issues the return-from-trap instruction.
On x86, for example, the processor will push the program counter, flags,
and a few other registers onto a per-process kernel stack; the return-from-
trap will pop these values off the stack and resume execution of the user-
mode program (see the Intel systems manuals [I11] for details). Other
hardware systems use different conventions, but the basic concepts are
similar across platforms.
There is one important detail left out of this discussion: how does the
trap know which code to run inside the OS? Clearly, the calling process
can’t specify an address to jump to (as you would when making a pro-
cedure call); doing so would allow programs to jump anywhere into the
kernel which clearly is a Very Bad Idea1. Thus the kernel must carefully
control what code executes upon a trap.
The kernel does so by setting up a trap table at boot time. When the
machine boots up, it does so in privileged (kernel) mode, and thus is free
to configure machine hardware as need be. One of the first things the OS
thus does is to tell the hardware what code to run when certain excep-
tional events occur. For example, what code should run when a hard-
disk interrupt takes place, when a keyboard interrupt occurs, or when
a program makes a system call? The OS informs the hardware of the
1Imagine jumping into code to access a file, but just after a permission check; in fact, it is
likely such an ability would enable a wily programmer to get the kernel to run arbitrary code
sequences [S07]. In general, try to avoid Very Bad Ideas like this one.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 5
OS @ boot Hardware
(kernel mode)
initialize trap table
remember address of...
syscall handler
OS @ run Hardware Program
(kernel mode) (user mode)
Create entry for process list
Allocate memory for program
Load program into memory
Setup user stack with argv
Fill kernel stack with reg/PC
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to main
Run main()
...
Call system call
trap into OS
save regs
(to kernel stack)
move to kernel mode
jump to trap handler
Handle trap
Do work of syscall
return-from-trap
restore regs
(from kernel stack)
move to user mode
jump to PC after trap
...
return from main
trap (via exit())
Free memory of process
Remove from process list
Figure 6.2: Limited Direct Execution Protocol
locations of these trap handlers, usually with some kind of special in-
struction. Once the hardware is informed, it remembers the location of
these handlers until the machine is next rebooted, and thus the hardware
knows what to do (i.e., what code to jump to) when system calls and other
exceptional events take place.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 MECHANISM: LIMITED DIRECT EXECUTION
TIP: BE WARY OF USER INPUTS IN SECURE SYSTEMS
Even though we have taken great pains to protect the OS during system
calls (by adding a hardware trapping mechanism, and ensuring all calls to
the OS are routed through it), there are still many other aspects to imple-
menting a secure operating system that we must consider. One of these
is the handling of arguments at the system call boundary; the OS must
check what the user passes in and ensure that arguments are properly
specified, or otherwise reject the call.
For example, with a write() system call, the user specifies an address
of a buffer as a source of the write call. If the user (either accidentally
or maliciously) passes in a “bad” address (e.g., one inside the kernel’s
portion of the address space), the OS must detect this and reject the call.
Otherwise, it would be possible for a user to read all of kernel memory;
given that kernel (virtual) memory also usually includes all of the physi-
cal memory of the system, this small slip would enable a program to read
the memory of any other process in the system.
In general, a secure system must treat user inputs with great suspicion.
Not doing so will undoubtedly lead to easily hacked software, a despair-
ing sense that the world is an unsafe and scary place, and the loss of job
security for the all-too-trusting OS developer.
To specify the exact system call, a system-call number is usually as-
signed to each system call. The user code is thus responsible for placing
the desired system-call number in a register or at a specified location on
the stack; the OS, when handling the system call inside the trap handler,
examines this number, ensures it is valid, and, if it is, executes the corre-
sponding code. This level of indirection serves as a form of protection;
user code cannot specify an exact address to jump to, but rather must
request a particular service via number.
One last aside: being able to execute the instruction to tell the hard-
ware where the trap tables are is a very powerful capability. Thus, as you
might have guessed, it is also a privileged operation. If you try to exe-
cute this instruction in user mode, the hardware won’t let you, and you
can probably guess what will happen (hint: adios, offending program).
Point to ponder: what horrible things could you do to a system if you
could install your own trap table? Could you take over the machine?
The timeline (with time increasing downward, in Figure 6.2) summa-
rizes the protocol. We assume each process has a kernel stack where reg-
isters (including general purpose registers and the program counter) are
saved to and restored from (by the hardware) when transitioning into and
out of the kernel.
There are two phases in the limited direct execution (LDE) protocol.
In the first (at boot time), the kernel initializes the trap table, and the
CPU remembers its location for subsequent use. The kernel does so via a
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 7
privileged instruction (all privileged instructions are highlighted in bold).
In the second (when running a process), the kernel sets up a few things
(e.g., allocating a node on the process list, allocating memory) before us-
ing a return-from-trap instruction to start the execution of the process;
this switches the CPU to user mode and begins running the process.
When the process wishes to issue a system call, it traps back into the OS,
which handles it and once again returns control via a return-from-trap
to the process. The process then completes its work, and returns from
main(); this usually will return into some stub code which will properly
exit the program (say, by calling the exit() system call, which traps into
the OS). At this point, the OS cleans up and we are done.
6.3 Problem #2: Switching Between Processes
The next problem with direct execution is achieving a switch between
processes. Switching between processes should be simple, right? The
OS should just decide to stop one process and start another. What’s the
big deal? But it actually is a little bit tricky: specifically, if a process is
running on the CPU, this by definition means the OS is not running. If
the OS is not running, how can it do anything at all? (hint: it can’t) While
this sounds almost philosophical, it is a real problem: there is clearly no
way for the OS to take an action if it is not running on the CPU. Thus we
arrive at the crux of the problem.
THE CRUX: HOW TO REGAIN CONTROL OF THE CPU
How can the operating system regain control of the CPU so that it can
switch between processes?
A Cooperative Approach: Wait For System Calls
One approach that some systems have taken in the past (for example,
early versions of the Macintosh operating system [M11], or the old Xerox
Alto system [A79]) is known as the cooperative approach. In this style,
the OS trusts the processes of the system to behave reasonably. Processes
that run for too long are assumed to periodically give up the CPU so that
the OS can decide to run some other task.
Thus, you might ask, how does a friendly process give up the CPU in
this utopian world? Most processes, as it turns out, transfer control of
the CPU to the OS quite frequently by making system calls, for example,
to open a file and subsequently read it, or to send a message to another
machine, or to create a new process. Systems like this often include an
explicit yield system call, which does nothing except to transfer control
to the OS so it can run other processes.
Applications also transfer control to the OS when they do something
illegal. For example, if an application divides by zero, or tries to access
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 MECHANISM: LIMITED DIRECT EXECUTION
memory that it shouldn’t be able to access, it will generate a trap to the
OS. The OS will then have control of the CPU again (and likely terminate
the offending process).
Thus, in a cooperative scheduling system, the OS regains control of
the CPU by waiting for a system call or an illegal operation of some kind
to take place. You might also be thinking: isn’t this passive approach less
than ideal? What happens, for example, if a process (whether malicious,
or just full of bugs) ends up in an infinite loop, and never makes a system
call? What can the OS do then?
A Non-Cooperative Approach: The OS Takes Control
Without some additional help from the hardware, it turns out the OS can’t
do much at all when a process refuses to make system calls (or mistakes)
and thus return control to the OS. In fact, in the cooperative approach,
your only recourse when a process gets stuck in an infinite loop is to
resort to the age-old solution to all problems in computer systems: reboot
the machine. Thus, we again arrive at a subproblem of our general quest
to gain control of the CPU.
THE CRUX: HOW TO GAIN CONTROL WITHOUT COOPERATION
How can the OS gain control of the CPU even if processes are not being
cooperative? What can the OS do to ensure a rogue process does not take
over the machine?
The answer turns out to be simple and was discovered by a number
of people building computer systems many years ago: a timer interrupt
[M+63]. A timer device can be programmed to raise an interrupt every
so many milliseconds; when the interrupt is raised, the currently running
process is halted, and a pre-configured interrupt handler in the OS runs.
At this point, the OS has regained control of the CPU, and thus can do
what it pleases: stop the current process, and start a different one.
As we discussed before with system calls, the OS must inform the
hardware of which code to run when the timer interrupt occurs; thus,
at boot time, the OS does exactly that. Second, also during the boot
TIP: DEALING WITH APPLICATION MISBEHAVIOR
Operating systems often have to deal with misbehaving processes, those
that either through design (maliciousness) or accident (bugs) attempt to
do something that they shouldn’t. In modern systems, the way the OS
tries to handle such malfeasance is to simply terminate the offender. One
strike and you’re out! Perhaps brutal, but what else should the OS do
when you try to access memory illegally or execute an illegal instruction?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 9
sequence, the OS must start the timer, which is of course a privileged
operation. Once the timer has begun, the OS can thus feel safe in that
control will eventually be returned to it, and thus the OS is free to run
user programs. The timer can also be turned off (also a privileged opera-
tion), something we will discuss later when we understand concurrency
in more detail.
Note that the hardware has some responsibility when an interrupt oc-
curs, in particular to save enough of the state of the program that was
running when the interrupt occurred such that a subsequent return-from-
trap instruction will be able to resume the running program correctly.
This set of actions is quite similar to the behavior of the hardware during
an explicit system-call trap into the kernel, with various registers thus
getting saved (e.g., onto a kernel stack) and thus easily restored by the
return-from-trap instruction.
Saving and Restoring Context
Now that the OS has regained control, whether cooperatively via a sys-
tem call, or more forcefully via a timer interrupt, a decision has to be
made: whether to continue running the currently-running process, or
switch to a different one. This decision is made by a part of the operating
system known as the scheduler; we will discuss scheduling policies in
great detail in the next few chapters.
If the decision is made to switch, the OS then executes a low-level
piece of code which we refer to as a context switch. A context switch is
conceptually simple: all the OS has to do is save a few register values
for the currently-executing process (onto its kernel stack, for example)
and restore a few for the soon-to-be-executing process (from its kernel
stack). By doing so, the OS thus ensures that when the return-from-trap
instruction is finally executed, instead of returning to the process that was
running, the system resumes execution of another process.
To save the context of the currently-running process, the OS will ex-
ecute some low-level assembly code to save the general purpose regis-
ters, PC, and the kernel stack pointer of the currently-running process,
and then restore said registers, PC, and switch to the kernel stack for the
soon-to-be-executing process. By switching stacks, the kernel enters the
call to the switch code in the context of one process (the one that was in-
terrupted) and returns in the context of another (the soon-to-be-executing
TIP: USE THE TIMER INTERRUPT TO REGAIN CONTROL
The addition of a timer interrupt gives the OS the ability to run again
on a CPU even if processes act in a non-cooperative fashion. Thus, this
hardware feature is essential in helping the OS maintain control of the
machine.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 MECHANISM: LIMITED DIRECT EXECUTION
TIP: REBOOT IS USEFUL
Earlier on, we noted that the only solution to infinite loops (and similar
behaviors) under cooperative preemption is to reboot the machine. While
you may scoff at this hack, researchers have shown that reboot (or in gen-
eral, starting over some piece of software) can be a hugely useful tool in
building robust systems [C+04].
Specifically, reboot is useful because it moves software back to a known
and likely more tested state. Reboots also reclaim stale or leaked re-
sources (e.g., memory) which may otherwise be hard to handle. Finally,
reboots are easy to automate. For all of these reasons, it is not uncommon
in large-scale cluster Internet services for system management software
to periodically reboot sets of machines in order to reset them and thus
obtain the advantages listed above.
Thus, next time you reboot, you are not just enacting some ugly hack.
Rather, you are using a time-tested approach to improving the behavior
of a computer system. Well done!
one). When the OS then finally executes a return-from-trap instruction,
the soon-to-be-executing process becomes the currently-running process.
And thus the context switch is complete.
A timeline of the entire process is shown in Figure 6.3. In this example,
Process A is running and then is interrupted by the timer interrupt. The
hardware saves its registers (onto its kernel stack) and enters the kernel
(switching to kernel mode). In the timer interrupt handler, the OS decides
to switch from running Process A to Process B. At that point, it calls the
switch() routine, which carefully saves current register values (into the
process structure of A), restores the registers of Process B (from its process
structure entry), and then switches contexts, specifically by changing the
stack pointer to use B’s kernel stack (and not A’s). Finally, the OS returns-
from-trap, which restores B’s registers and starts running it.
Note that there are two types of register saves/restores that happen
during this protocol. The first is when the timer interrupt occurs; in this
case, the user registers of the running process are implicitly saved by the
hardware, using the kernel stack of that process. The second is when the
OS decides to switch from A to B; in this case, the kernel registers are ex-
plicitly saved by the software (i.e., the OS), but this time into memory in
the process structure of the process. The latter action moves the system
from running as if it just trapped into the kernel from A to as if it just
trapped into the kernel from B.
To give you a better sense of how such a switch is enacted, Figure 6.4
shows the context switch code for xv6. See if you can make sense of it
(you’ll have to know a bit of x86, as well as some xv6, to do so). The
context structures old and new are found in the old and new process’s
process structures, respectively.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 11
OS @ boot Hardware
(kernel mode)
initialize trap table
remember addresses of...
syscall handler
timer handler
start interrupt timer
start timer
interrupt CPU in X ms
OS @ run Hardware Program
(kernel mode) (user mode)
Process A
...
timer interrupt
save regs(A) → k-stack(A)
move to kernel mode
jump to trap handler
Handle the trap
Call switch() routine
save regs(A) → proc t(A)
restore regs(B) ← proc t(B)
switch to k-stack(B)
return-from-trap (into B)
restore regs(B) ← k-stack(B)
move to user mode
jump to B’s PC
Process B
...
Figure 6.3: Limited Direct Execution Protocol (Timer Interrupt)
6.4 Worried About Concurrency?
Some of you, as attentive and thoughtful readers, may be now think-
ing: “Hmm... what happens when, during a system call, a timer interrupt
occurs?” or “What happens when you’re handling one interrupt and an-
other one happens? Doesn’t that get hard to handle in the kernel?” Good
questions — we really have some hope for you yet!
The answer is yes, the OS does indeed need to be concerned as to what
happens if, during interrupt or trap handling, another interrupt occurs.
This, in fact, is the exact topic of the entire second piece of this book, on
concurrency; we’ll defer a detailed discussion until then.
To whet your appetite, we’ll just sketch some basics of how the OS
handles these tricky situations. One simple thing an OS might do is dis-
able interrupts during interrupt processing; doing so ensures that when
one interrupt is being handled, no other one will be delivered to the CPU.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 MECHANISM: LIMITED DIRECT EXECUTION
1 # void swtch(struct context *old, struct context *new);
2 #
3 # Save current register context in old
4 # and then load register context from new.
5 .globl swtch
6 swtch:
7 # Save old registers
8 movl 4(%esp), %eax # put old ptr into eax
9 popl 0(%eax) # save the old IP
10 movl %esp, 4(%eax) # and stack
11 movl %ebx, 8(%eax) # and other registers
12 movl %ecx, 12(%eax)
13 movl %edx, 16(%eax)
14 movl %esi, 20(%eax)
15 movl %edi, 24(%eax)
16 movl %ebp, 28(%eax)
17
18 # Load new registers
19 movl 4(%esp), %eax # put new ptr into eax
20 movl 28(%eax), %ebp # restore other registers
21 movl 24(%eax), %edi
22 movl 20(%eax), %esi
23 movl 16(%eax), %edx
24 movl 12(%eax), %ecx
25 movl 8(%eax), %ebx
26 movl 4(%eax), %esp # stack is switched here
27 pushl 0(%eax) # return addr put in place
28 ret # finally return into new ctxt
Figure 6.4: The xv6 Context Switch Code
Of course, the OS has to be careful in doing so; disabling interrupts for
too long could lead to lost interrupts, which is (in technical terms) bad.
Operating systems also have developed a number of sophisticated
locking schemes to protect concurrent access to internal data structures.
This enables multiple activities to be on-going within the kernel at the
same time, particularly useful on multiprocessors. As we’ll see in the
next piece of this book on concurrency, though, such locking can be com-
plicated and lead to a variety of interesting and hard-to-find bugs.
6.5 Summary
We have described some key low-level mechanisms to implement CPU
virtualization, a set of techniques which we collectively refer to as limited
direct execution. The basic idea is straightforward: just run the program
you want to run on the CPU, but first make sure to set up the hardware
so as to limit what the process can do without OS assistance.
This general approach is taken in real life as well. For example, those
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 13
ASIDE: HOW LONG CONTEXT SWITCHES TAKE
A natural question you might have is: how long does something like a
context switch take? Or even a system call? For those of you that are cu-
rious, there is a tool called lmbench [MS96] that measures exactly those
things, as well as a few other performance measures that might be rele-
vant.
Results have improved quite a bit over time, roughly tracking processor
performance. For example, in 1996 running Linux 1.3.37 on a 200-MHz
P6 CPU, system calls took roughly 4 microseconds, and a context switch
roughly 6 microseconds [MS96]. Modern systems perform almost an or-
der of magnitude better, with sub-microsecond results on systems with
2- or 3-GHz processors.
It should be noted that not all operating-system actions track CPU per-
formance. As Ousterhout observed, many OS operations are memory
intensive, and memory bandwidth has not improved as dramatically as
processor speed over time [O90]. Thus, depending on your workload,
buying the latest and greatest processor may not speed up your OS as
much as you might hope.
of you who have children, or, at least, have heard of children, may be
familiar with the concept of baby proofing a room: locking cabinets con-
taining dangerous stuff and covering electrical sockets. When the room is
thus readied, you can let your baby roam freely, secure in the knowledge
that the most dangerous aspects of the room have been restricted.
In an analogous manner, the OS “baby proofs” the CPU, by first (dur-
ing boot time) setting up the trap handlers and starting an interrupt timer,
and then by only running processes in a restricted mode. By doing so, the
OS can feel quite assured that processes can run efficiently, only requir-
ing OS intervention to perform privileged operations or when they have
monopolized the CPU for too long and thus need to be switched out.
We thus have the basic mechanisms for virtualizing the CPU in place.
But a major question is left unanswered: which process should we run at
a given time? It is this question that the scheduler must answer, and thus
the next topic of our study.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
14 MECHANISM: LIMITED DIRECT EXECUTION
ASIDE: KEY CPU VIRTUALIZATION TERMS (MECHANISMS)
• The CPU should support at least two modes of execution: a re-
stricted user mode and a privileged (non-restricted) kernel mode.
• Typical user applications run in user mode, and use a system call
to trap into the kernel to request operating system services.
• The trap instruction saves register state carefully, changes the hard-
ware status to kernel mode, and jumps into the OS to a pre-specified
destination: the trap table.
• When the OS finishes servicing a system call, it returns to the user
program via another special return-from-trap instruction, which re-
duces privilege and returns control to the instruction after the trap
that jumped into the OS.
• The trap tables must be set up by the OS at boot time, and make
sure that they cannot be readily modified by user programs. All
of this is part of the limited direct execution protocol which runs
programs efficiently but without loss of OS control.
• Once a program is running, the OS must use hardware mechanisms
to ensure the user program does not run forever, namely the timer
interrupt. This approach is a non-cooperative approach to CPU
scheduling.
• Sometimes the OS, during a timer interrupt or system call, might
wish to switch from running the current process to a different one,
a low-level technique known as a context switch.
.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
MECHANISM: LIMITED DIRECT EXECUTION 15
References
[A79] “Alto User’s Handbook” by Xerox. Xerox Palo Alto Research Center, September 1979.
Available: http://history-computer.com/Library/AltoUsersHandbook.pdf. An
amazing system, way ahead of its time. Became famous because Steve Jobs visited, took notes, and built
Lisa and eventually Mac.
[C+04] “Microreboot — A Technique for Cheap Recovery” by G. Candea, S. Kawamoto, Y.
Fujiki, G. Friedman, A. Fox. OSDI ’04, San Francisco, CA, December 2004. An excellent paper
pointing out how far one can go with reboot in building more robust systems.
[I11] “Intel 64 and IA-32 Architectures Software Developer’s Manual” by Volume 3A and 3B:
System Programming Guide. Intel Corporation, January 2011. This is just a boring manual, but
sometimes those are useful.
[K+61] “One-Level Storage System” by T. Kilburn, D.B.G. Edwards, M.J. Lanigan, F.H. Sumner.
IRE Transactions on Electronic Computers, April 1962. The Atlas pioneered much of what you see
in modern systems. However, this paper is not the best one to read. If you were to only read one, you
might try the historical perspective below [L78].
[L78] “The Manchester Mark I and Atlas: A Historical Perspective” by S. H. Lavington. Com-
munications of the ACM, 21:1, January 1978. A history of the early development of computers and
the pioneering efforts of Atlas.
[M+63] “A Time-Sharing Debugging System for a Small Computer” by J. McCarthy, S. Boilen,
E. Fredkin, J. C. R. Licklider. AFIPS ’63 (Spring), May, 1963, New York, USA. An early paper
about time-sharing that refers to using a timer interrupt; the quote that discusses it: “The basic task of
the channel 17 clock routine is to decide whether to remove the current user from core and if so to decide
which user program to swap in as he goes out.”
[MS96] “lmbench: Portable tools for performance analysis” by Larry McVoy and Carl Staelin.
USENIX Annual Technical Conference, January 1996. A fun paper about how to measure a number
of different things about your OS and its performance. Download lmbench and give it a try.
[M11] “Mac OS 9” by Apple Computer, Inc.. January 2011. Available at the following URL:
http://en.wikipedia.org/wiki/Mac OS 9 . You can probably even find an OS 9 emulator
out there if you want to; check it out, it’s a fun little Mac!
[O90] “Why Aren’t Operating Systems Getting Faster as Fast as Hardware?” by J. Ouster-
hout. USENIX Summer Conference, June 1990. A classic paper on the nature of operating system
performance.
[P10] “The Single UNIX Specification, Version 3” by The Open Group, May 2010. Available:
http://www.unix.org/version3/. This is hard and painful to read, so probably avoid it if you
can. Like, unless someone is paying you to read it. Or, you’re just so curious you can’t help it!
[S07] “The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls
(on the x86)” by Hovav Shacham. CCS ’07, October 2007. One of those awesome, mind-blowing
ideas that you’ll see in research from time to time. The author shows that if you can jump into code
arbitrarily, you can essentially stitch together any code sequence you like (given a large code base); read
the paper for the details. The technique makes it even harder to defend against malicious attacks, alas.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
16 MECHANISM: LIMITED DIRECT EXECUTION
Homework (Measurement)
ASIDE: MEASUREMENT HOMEWORKS
Measurement homeworks are small exercises where you write code to
run on a real machine, in order to measure some aspect of OS or hardware
performance. The idea behind such homeworks is to give you a little bit
of hands-on experience with a real operating system.
In this homework, you’ll measure the costs of a system call and context
switch. Measuring the cost of a system call is relatively easy. For example,
you could repeatedly call a simple system call (e.g., performing a 0-byte
read), and time how long it takes; dividing the time by the number of
iterations gives you an estimate of the cost of a system call.
One thing you’ll have to take into account is the precision and accu-
racy of your timer. A typical timer that you can use is gettimeofday();
read the man page for details. What you’ll see there is that gettimeofday()
returns the time in microseconds since 1970; however, this does not mean
that the timer is precise to the microsecond. Measure back-to-back calls
to gettimeofday() to learn something about how precise the timer re-
ally is; this will tell you how many iterations of your null system-call
test you’ll have to run in order to get a good measurement result. If
gettimeofday() is not precise enough for you, you might look into
using the rdtsc instruction available on x86 machines.
Measuring the cost of a context switch is a little trickier. The lmbench
benchmark does so by running two processes on a single CPU, and set-
ting up two UNIX pipes between them; a pipe is just one of many ways
processes in a UNIX system can communicate with one another. The first
process then issues a write to the first pipe, and waits for a read on the
second; upon seeing the first process waiting for something to read from
the second pipe, the OS puts the first process in the blocked state, and
switches to the other process, which reads from the first pipe and then
writes to the second. When the second process tries to read from the first
pipe again, it blocks, and thus the back-and-forth cycle of communication
continues. By measuring the cost of communicating like this repeatedly,
lmbench can make a good estimate of the cost of a context switch. You
can try to re-create something similar here, using pipes, or perhaps some
other communication mechanism such as UNIX sockets.
One difficulty in measuring context-switch cost arises in systems with
more than one CPU; what you need to do on such a system is ensure that
your context-switching processes are located on the same processor. For-
tunately, most operating systems have calls to bind a process to a partic-
ular processor; on Linux, for example, the sched setaffinity() call
is what you’re looking for. By ensuring both processes are on the same
processor, you are making sure to measure the cost of the OS stopping
one process and restoring another on the same CPU.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG

7
Scheduling: Introduction
By now low-level mechanisms of running processes (e.g., context switch-
ing) should be clear; if they are not, go back a chapter or two, and read the
description of how that stuff works again. However, we have yet to un-
derstand the high-level policies that an OS scheduler employs. We will
now do just that, presenting a series of scheduling policies (sometimes
called disciplines) that various smart and hard-working people have de-
veloped over the years.
The origins of scheduling, in fact, predate computer systems; early
approaches were taken from the field of operations management and ap-
plied to computers. This reality should be no surprise: assembly lines
and many other human endeavors also require scheduling, and many of
the same concerns exist therein, including a laser-like desire for efficiency.
And thus, our problem:
THE CRUX: HOW TO DEVELOP SCHEDULING POLICY
How should we develop a basic framework for thinking about
scheduling policies? What are the key assumptions? What metrics are
important? What basic approaches have been used in the earliest of com-
puter systems?
7.1 Workload Assumptions
Before getting into the range of possible policies, let us first make a
number of simplifying assumptions about the processes running in the
system, sometimes collectively called the workload. Determining the
workload is a critical part of building policies, and the more you know
about workload, the more fine-tuned your policy can be.
The workload assumptions we make here are mostly unrealistic, but
that is alright (for now), because we will relax them as we go, and even-
tually develop what we will refer to as ... (dramatic pause) ...
1
2 SCHEDULING: INTRODUCTION
a fully-operational scheduling discipline1.
We will make the following assumptions about the processes, some-
times called jobs, that are running in the system:
1. Each job runs for the same amount of time.
2. All jobs arrive at the same time.
3. Once started, each job runs to completion.
4. All jobs only use the CPU (i.e., they perform no I/O)
5. The run-time of each job is known.
We said many of these assumptions were unrealistic, but just as some
animals are more equal than others in Orwell’s Animal Farm [O45], some
assumptions are more unrealistic than others in this chapter. In particu-
lar, it might bother you that the run-time of each job is known: this would
make the scheduler omniscient, which, although it would be great (prob-
ably), is not likely to happen anytime soon.
7.2 Scheduling Metrics
Beyond making workload assumptions, we also need one more thing
to enable us to compare different scheduling policies: a scheduling met-
ric. A metric is just something that we use to measure something, and
there are a number of different metrics that make sense in scheduling.
For now, however, let us also simplify our life by simply having a sin-
gle metric: turnaround time. The turnaround time of a job is defined
as the time at which the job completes minus the time at which the job
arrived in the system. More formally, the turnaround time Tturnaround is:
Tturnaround = Tcompletion − Tarrival (7.1)
Because we have assumed that all jobs arrive at the same time, for now
Tarrival = 0 and hence Tturnaround = Tcompletion. This fact will change
as we relax the aforementioned assumptions.
You should note that turnaround time is a performance metric, which
will be our primary focus this chapter. Another metric of interest is fair-
ness, as measured (for example) by Jain’s Fairness Index [J91]. Perfor-
mance and fairness are often at odds in scheduling; a scheduler, for ex-
ample, may optimize performance but at the cost of preventing a few jobs
from running, thus decreasing fairness. This conundrum shows us that
life isn’t always perfect.
7.3 First In, First Out (FIFO)
The most basic algorithm we can implement is known as First In, First
Out (FIFO) scheduling or sometimes First Come, First Served (FCFS).
1Said in the same way you would say “A fully-operational Death Star.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 3
FIFO has a number of positive properties: it is clearly simple and thus
easy to implement. And, given our assumptions, it works pretty well.
Let’s do a quick example together. Imagine three jobs arrive in the
system, A, B, and C, at roughly the same time (Tarrival = 0). Because
FIFO has to put some job first, let’s assume that while they all arrived
simultaneously, A arrived just a hair before B which arrived just a hair
before C. Assume also that each job runs for 10 seconds. What will the
average turnaround time be for these jobs?
0 20 40 60 80 100 120
Time
A B C
Figure 7.1: FIFO Simple Example
From Figure 7.1, you can see that A finished at 10, B at 20, and C at 30.
Thus, the average turnaround time for the three jobs is simply 10+20+30
3 =
20. Computing turnaround time is as easy as that.
Now let’s relax one of our assumptions. In particular, let’s relax as-
sumption 1, and thus no longer assume that each job runs for the same
amount of time. How does FIFO perform now? What kind of workload
could you construct to make FIFO perform poorly?
(think about this before reading on ... keep thinking ... got it?!)
Presumably you’ve figured this out by now, but just in case, let’s do
an example to show how jobs of different lengths can lead to trouble for
FIFO scheduling. In particular, let’s again assume three jobs (A, B, and
C), but this time A runs for 100 seconds while B and C run for 10 each.
0 20 40 60 80 100 120
Time
A B C
Figure 7.2: Why FIFO Is Not That Great
As you can see in Figure 7.2, Job A runs first for the full 100 seconds
before B or C even get a chance to run. Thus, the average turnaround
time for the system is high: a painful 110 seconds ( 100+110+120
3 = 110).
This problem is generally referred to as the convoy effect [B+79], where
a number of relatively-short potential consumers of a resource get queued
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
4 SCHEDULING: INTRODUCTION
TIP: THE PRINCIPLE OF SJF
Shortest Job First represents a general scheduling principle that can be
applied to any system where the perceived turnaround time per customer
(or, in our case, a job) matters. Think of any line you have waited in: if
the establishment in question cares about customer satisfaction, it is likely
they have taken SJF into account. For example, grocery stores commonly
have a “ten-items-or-less” line to ensure that shoppers with only a few
things to purchase don’t get stuck behind the family preparing for some
upcoming nuclear winter.
behind a heavyweight resource consumer. This scheduling scenario might
remind you of a single line at a grocery store and what you feel like when
you see the person in front of you with three carts full of provisions and
a checkbook out; it’s going to be a while2.
So what should we do? How can we develop a better algorithm to
deal with our new reality of jobs that run for different amounts of time?
Think about it first; then read on.
7.4 Shortest Job First (SJF)
It turns out that a very simple approach solves this problem; in fact
it is an idea stolen from operations research [C54,PV56] and applied to
scheduling of jobs in computer systems. This new scheduling discipline
is known as Shortest Job First (SJF), and the name should be easy to
remember because it describes the policy quite completely: it runs the
shortest job first, then the next shortest, and so on.
0 20 40 60 80 100 120
Time
B C A
Figure 7.3: SJF Simple Example
Let’s take our example above but with SJF as our scheduling policy.
Figure 7.3 shows the results of running A, B, and C. Hopefully the dia-
gram makes it clear why SJF performs much better with regards to aver-
age turnaround time. Simply by running B and C before A, SJF reduces
average turnaround from 110 seconds to 50 ( 10+20+120
3 = 50), more than
a factor of two improvement.
2Recommended action in this case: either quickly switch to a different line, or take a long,
deep, and relaxing breath. That’s right, breathe in, breathe out. It will be OK, don’t worry.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 5
ASIDE: PREEMPTIVE SCHEDULERS
In the old days of batch computing, a number of non-preemptive sched-
ulers were developed; such systems would run each job to completion
before considering whether to run a new job. Virtually all modern sched-
ulers are preemptive, and quite willing to stop one process from run-
ning in order to run another. This implies that the scheduler employs the
mechanisms we learned about previously; in particular, the scheduler can
perform a context switch, stopping one running process temporarily and
resuming (or starting) another.
In fact, given our assumptions about jobs all arriving at the same time,
we could prove that SJF is indeed an optimal scheduling algorithm. How-
ever, you are in a systems class, not theory or operations research; no
proofs are allowed.
Thus we arrive upon a good approach to scheduling with SJF, but our
assumptions are still fairly unrealistic. Let’s relax another. In particular,
we can target assumption 2, and now assume that jobs can arrive at any
time instead of all at once. What problems does this lead to?
(Another pause to think ... are you thinking? Come on, you can do it)
Here we can illustrate the problem again with an example. This time,
assume A arrives at t = 0 and needs to run for 100 seconds, whereas B
and C arrive at t = 10 and each need to run for 10 seconds. With pure
SJF, we’d get the schedule seen in Figure 7.4.
0 20 40 60 80 100 120
Time
A B C
[B,C arrive]
Figure 7.4: SJF With Late Arrivals From B and C
As you can see from the figure, even though B and C arrived shortly
after A, they still are forced to wait until A has completed, and thus suffer
the same convoy problem. Average turnaround time for these three jobs
is 103.33 seconds ( 100+(110−10)+(120−10)
3 ). What can a scheduler do?
7.5 Shortest Time-to-Completion First (STCF)
To address this concern, we need to relax assumption 3 (that jobs must
run to completion), so let’s do that. We also need some machinery within
the scheduler itself. As you might have guessed, given our previous dis-
cussion about timer interrupts and context switching, the scheduler can
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
6 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120
Time
A B C A
[B,C arrive]
Figure 7.5: STCF Simple Example
certainly do something else when B and C arrive: it can preempt job A
and decide to run another job, perhaps continuing A later. SJF by our defi-
nition is a non-preemptive scheduler, and thus suffers from the problems
described above.
Fortunately, there is a scheduler which does exactly that: add preemp-
tion to SJF, known as the Shortest Time-to-Completion First (STCF) or
Preemptive Shortest Job First (PSJF) scheduler [CK68]. Any time a new
job enters the system, the STCF scheduler determines which of the re-
maining jobs (including the new job) has the least time left, and schedules
that one. Thus, in our example, STCF would preempt A and run B and C
to completion; only when they are finished would A’s remaining time be
scheduled. Figure 7.5 shows an example.
The result is a much-improved average turnaround time: 50 seconds
( (120−0)+(20−10)+(30−10)
3 ). And as before, given our new assumptions,
STCF is provably optimal; given that SJF is optimal if all jobs arrive at
the same time, you should probably be able to see the intuition behind
the optimality of STCF.
7.6 A New Metric: Response Time
Thus, if we knew job lengths, and that jobs only used the CPU, and our
only metric was turnaround time, STCF would be a great policy. In fact,
for a number of early batch computing systems, these types of scheduling
algorithms made some sense. However, the introduction of time-shared
machines changed all that. Now users would sit at a terminal and de-
mand interactive performance from the system as well. And thus, a new
metric was born: response time.
We define response time as the time from when the job arrives in a
system to the first time it is scheduled3. More formally:
Tresponse = Tf irstrun − Tarrival (7.2)
3Some define it slightly differently, e.g., to also include the time until the job produces
some kind of “response”; our definition is the best-case version of this, essentially assuming
that the job produces a response instantaneously.
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 7
0 5 10 15 20 25 30
Time
A B C
Figure 7.6: SJF Again (Bad for Response Time)
0 5 10 15 20 25 30
Time
ABCABCABCABCABC
Figure 7.7: Round Robin (Good For Response Time)
For example, if we had the schedule from Figure 7.5 (with A arriving
at time 0, and B and C at time 10), the response time of each job is as
follows: 0 for job A, 0 for B, and 10 for C (average: 3.33).
As you might be thinking, STCF and related disciplines are not par-
ticularly good for response time. If three jobs arrive at the same time,
for example, the third job has to wait for the previous two jobs to run in
their entirety before being scheduled just once. While great for turnaround
time, this approach is quite bad for response time and interactivity. In-
deed, imagine sitting at a terminal, typing, and having to wait 10 seconds
to see a response from the system just because some other job got sched-
uled in front of yours: not too pleasant.
Thus, we are left with another problem: how can we build a scheduler
that is sensitive to response time?
7.7 Round Robin
To solve this problem, we will introduce a new scheduling algorithm,
classically referred to as Round-Robin (RR) scheduling [K64]. The basic
idea is simple: instead of running jobs to completion, RR runs a job for a
time slice (sometimes called a scheduling quantum) and then switches
to the next job in the run queue. It repeatedly does so until the jobs are
finished. For this reason, RR is sometimes called time-slicing. Note that
the length of a time slice must be a multiple of the timer-interrupt period;
thus if the timer interrupts every 10 milliseconds, the time slice could be
10, 20, or any other multiple of 10 ms.
To understand RR in more detail, let’s look at an example. Assume
three jobs A, B, and C arrive at the same time in the system, and that
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
8 SCHEDULING: INTRODUCTION
TIP: AMORTIZATION CAN REDUCE COSTS
The general technique of amortization is commonly used in systems
when there is a fixed cost to some operation. By incurring that cost less
often (i.e., by performing the operation fewer times), the total cost to the
system is reduced. For example, if the time slice is set to 10 ms, and the
context-switch cost is 1 ms, roughly 10% of time is spent context switch-
ing and is thus wasted. If we want to amortize this cost, we can increase
the time slice, e.g., to 100 ms. In this case, less than 1% of time is spent
context switching, and thus the cost of time-slicing has been amortized.
they each wish to run for 5 seconds. An SJF scheduler runs each job to
completion before running another (Figure 7.6). In contrast, RR with a
time-slice of 1 second would cycle through the jobs quickly (Figure 7.7).
The average response time of RR is: 0+1+2
3 = 1; for SJF, average re-
sponse time is: 0+5+10
3 = 5.
As you can see, the length of the time slice is critical for RR. The shorter
it is, the better the performance of RR under the response-time metric.
However, making the time slice too short is problematic: suddenly the
cost of context switching will dominate overall performance. Thus, de-
ciding on the length of the time slice presents a trade-off to a system de-
signer, making it long enough to amortize the cost of switching without
making it so long that the system is no longer responsive.
Note that the cost of context switching does not arise solely from the
OS actions of saving and restoring a few registers. When programs run,
they build up a great deal of state in CPU caches, TLBs, branch predictors,
and other on-chip hardware. Switching to another job causes this state
to be flushed and new state relevant to the currently-running job to be
brought in, which may exact a noticeable performance cost [MB91].
RR, with a reasonable time slice, is thus an excellent scheduler if re-
sponse time is our only metric. But what about our old friend turnaround
time? Let’s look at our example above again. A, B, and C, each with run-
ning times of 5 seconds, arrive at the same time, and RR is the scheduler
with a (long) 1-second time slice. We can see from the picture above that
A finishes at 13, B at 14, and C at 15, for an average of 14. Pretty awful!
It is not surprising, then, that RR is indeed one of the worst policies if
turnaround time is our metric. Intuitively, this should make sense: what
RR is doing is stretching out each job as long as it can, by only running
each job for a short bit before moving to the next. Because turnaround
time only cares about when jobs finish, RR is nearly pessimal, even worse
than simple FIFO in many cases.
More generally, any policy (such as RR) that is fair, i.e., that evenly di-
vides the CPU among active processes on a small time scale, will perform
poorly on metrics such as turnaround time. Indeed, this is an inherent
trade-off: if you are willing to be unfair, you can run shorter jobs to com-
pletion, but at the cost of response time; if you instead value fairness,
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 9
TIP: OVERLAP ENABLES HIGHER UTILIZATION
When possible, overlap operations to maximize the utilization of sys-
tems. Overlap is useful in many different domains, including when per-
forming disk I/O or sending messages to remote machines; in either case,
starting the operation and then switching to other work is a good idea,
and improves the overall utilization and efficiency of the system.
response time is lowered, but at the cost of turnaround time. This type of
trade-off is common in systems; you can’t have your cake and eat it too4.
We have developed two types of schedulers. The first type (SJF, STCF)
optimizes turnaround time, but is bad for response time. The second type
(RR) optimizes response time but is bad for turnaround. And we still
have two assumptions which need to be relaxed: assumption 4 (that jobs
do no I/O), and assumption 5 (that the run-time of each job is known).
Let’s tackle those assumptions next.
7.8 Incorporating I/O
First we will relax assumption 4 — of course all programs perform
I/O. Imagine a program that didn’t take any input: it would produce the
same output each time. Imagine one without output: it is the proverbial
tree falling in the forest, with no one to see it; it doesn’t matter that it ran.
A scheduler clearly has a decision to make when a job initiates an I/O
request, because the currently-running job won’t be using the CPU dur-
ing the I/O; it is blocked waiting for I/O completion. If the I/O is sent to
a hard disk drive, the process might be blocked for a few milliseconds or
longer, depending on the current I/O load of the drive. Thus, the sched-
uler should probably schedule another job on the CPU at that time.
The scheduler also has to make a decision when the I/O completes.
When that occurs, an interrupt is raised, and the OS runs and moves
the process that issued the I/O from blocked back to the ready state. Of
course, it could even decide to run the job at that point. How should the
OS treat each job?
To understand this issue better, let us assume we have two jobs, A and
B, which each need 50 ms of CPU time. However, there is one obvious
difference: A runs for 10 ms and then issues an I/O request (assume here
that I/Os each take 10 ms), whereas B simply uses the CPU for 50 ms and
performs no I/O. The scheduler runs A first, then B after (Figure 7.8).
Assume we are trying to build a STCF scheduler. How should such a
scheduler account for the fact that A is broken up into 5 10-ms sub-jobs,
4A saying that confuses people, because it should be “You can’t keep your cake and eat it
too” (which is kind of obvious, no?). Amazingly, there is a wikipedia page about this saying;
even more amazingly, it is kind of fun to read [W15]. As they say in Italian, you can’t Avere la
botte piena e la moglie ubriaca.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
10 SCHEDULING: INTRODUCTION
0 20 40 60 80 100 120 140
Time
A A A A A B B B B B
CPU
Disk
Figure 7.8: Poor Use Of Resources
0 20 40 60 80 100 120 140
Time
A A A A AB B B B B
CPU
Disk
Figure 7.9: Overlap Allows Better Use Of Resources
whereas B is just a single 50-ms CPU demand? Clearly, just running one
job and then the other without considering how to take I/O into account
makes little sense.
A common approach is to treat each 10-ms sub-job of A as an indepen-
dent job. Thus, when the system starts, its choice is whether to schedule
a 10-ms A or a 50-ms B. With STCF, the choice is clear: choose the shorter
one, in this case A. Then, when the first sub-job of A has completed, only
B is left, and it begins running. Then a new sub-job of A is submitted,
and it preempts B and runs for 10 ms. Doing so allows for overlap, with
the CPU being used by one process while waiting for the I/O of another
process to complete; the system is thus better utilized (see Figure 7.9).
And thus we see how a scheduler might incorporate I/O. By treating
each CPU burst as a job, the scheduler makes sure processes that are “in-
teractive” get run frequently. While those interactive jobs are performing
I/O, other CPU-intensive jobs run, thus better utilizing the processor.
7.9 No More Oracle
With a basic approach to I/O in place, we come to our final assump-
tion: that the scheduler knows the length of each job. As we said before,
this is likely the worst assumption we could make. In fact, in a general-
purpose OS (like the ones we care about), the OS usually knows very little
about the length of each job. Thus, how can we build an approach that be-
haves like SJF/STCF without such a priori knowledge? Further, how can
we incorporate some of the ideas we have seen with the RR scheduler so
that response time is also quite good?
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 11
7.10 Summary
We have introduced the basic ideas behind scheduling and developed
two families of approaches. The first runs the shortest job remaining and
thus optimizes turnaround time; the second alternates between all jobs
and thus optimizes response time. Both are bad where the other is good,
alas, an inherent trade-off common in systems. We have also seen how we
might incorporate I/O into the picture, but have still not solved the prob-
lem of the fundamental inability of the OS to see into the future. Shortly,
we will see how to overcome this problem, by building a scheduler that
uses the recent past to predict the future. This scheduler is known as the
multi-level feedback queue, and it is the topic of the next chapter.
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES
12 SCHEDULING: INTRODUCTION
References
[B+79] “The Convoy Phenomenon” by M. Blasgen, J. Gray, M. Mitoma, T. Price. ACM Op-
erating Systems Review, 13:2, April 1979. Perhaps the first reference to convoys, which occurs in
databases as well as the OS.
[C54] “Priority Assignment in Waiting Line Problems” by A. Cobham. Journal of Operations
Research, 2:70, pages 70–76, 1954. The pioneering paper on using an SJF approach in scheduling the
repair of machines.
[K64] “Analysis of a Time-Shared Processor” by Leonard Kleinrock. Naval Research Logistics
Quarterly, 11:1, pages 59–73, March 1964. May be the first reference to the round-robin scheduling
algorithm; certainly one of the first analyses of said approach to scheduling a time-shared system.
[CK68] “Computer Scheduling Methods and their Countermeasures” by Edward G. Coffman
and Leonard Kleinrock. AFIPS ’68 (Spring), April 1968. An excellent early introduction to and
analysis of a number of basic scheduling disciplines.
[J91] “The Art of Computer Systems Performance Analysis: Techniques for Experimental De-
sign, Measurement, Simulation, and Modeling” by R. Jain. Interscience, New York, April 1991.
The standard text on computer systems measurement. A great reference for your library, for sure.
[O45] “Animal Farm” by George Orwell. Secker and Warburg (London), 1945. A great but
depressing allegorical book about power and its corruptions. Some say it is a critique of Stalin and the
pre-WWII Stalin era in the U.S.S.R; we say it’s a critique of pigs.
[PV56] “Machine Repair as a Priority Waiting-Line Problem” by Thomas E. Phipps Jr., W. R.
Van Voorhis. Operations Research, 4:1, pages 76–86, February 1956. Follow-on work that gen-
eralizes the SJF approach to machine repair from Cobham’s original work; also postulates the utility of
an STCF approach in such an environment. Specifically, “There are certain types of repair work, ...
involving much dismantling and covering the floor with nuts and bolts, which certainly should not be
interrupted once undertaken; in other cases it would be inadvisable to continue work on a long job if one
or more short ones became available (p.81).”
[MB91] “The effect of context switches on cache performance” by Jeffrey C. Mogul, Anita Borg.
ASPLOS, 1991. A nice study on how cache performance can be affected by context switching; less of an
issue in today’s systems where processors issue billions of instructions per second but context-switches
still happen in the millisecond time range.
[W15] “You can’t have your cake and eat it” by Authors: Unknown.. Wikipedia (as of Decem-
ber 2015). http://en.wikipedia.org/wiki/You can’t have your cake and eat it.
The best part of this page is reading all the similar idioms from other languages. In Tamil, you can’t
“have both the moustache and drink the soup.”
OPERATING
SYSTEMS
[VERSION 1.10]
WWW.OSTEP.ORG
SCHEDULING: INTRODUCTION 13
Homework (Simulation)
This program, scheduler.py, allows you to see how different sched-
ulers perform under scheduling metrics such as response time, turnaround
time, and total wait time. See the README for details.
Questions
1. Compute the response time and turnaround time when running
three jobs of length 200 with the SJF and FIFO schedulers.
2. Now do the same but with jobs of different lengths: 100, 200, and
300.
3. Now do the same, but also with the RR scheduler and a time-slice
of 1.
4. For what types of workloads does SJF deliver the same turnaround
times as FIFO?
5. For what types of workloads and quantum lengths does SJF deliver
the same response times as RR?
6. What happens to response time with SJF as job lengths increase?
Can you use the simulator to demonstrate the trend?
7. What happens to response time with RR as quantum lengths in-
crease? Can you write an equation that gives the worst-case re-
sponse time, given N jobs?
© 2008–23, ARPACI-DUSSEAU THREE
EASY
PIECES 



































